/*
Copyright (c) 2009, Young Digital Planet SA
All rights reserved.

* Redistribution and use in source and binary forms, with or without 
  modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this 
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the Young Digital Planet SA nor the names of its 
  contributors may be used to endorse or promote products derived from this 
  software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
package pl.ydp.utils
{
	import com.yauthor.utils.IPathBase;
	
	import flash.utils.Dictionary;
	
	import mx.utils.StringUtil;
	
	/**
	 * Narzędzia do operowania na XML'ach
	 */
	public class XMLUtil
	{
		/**
		 * XML parser gdy nie ingoruje whitespace'ów to niepotrafi sparsować
		 * xml zaczynającego się od <? xml... ?>\n - not well formed
		 */
		public static function normalizeWhitespaceXML( data:String ):String {
			if ( data.indexOf( "<?" )==0 ) {
				var i2:int = data.indexOf( "?>" );
				i2 +=2 ;
				
				// muszę usunąć whitespace'y na końcu i na początku
				// inaczej parser Flex'a uznaje, że są to dodatkowe
				// elementy psujące well-formness dokumentu
				data = StringUtil.trim( data.substring( i2 ) );
			}
			
			return data;
		}
		
		/**
		 * RegExp match'ujący wszystkie białe znaki w XML'u
		 */
		public static const WHITESPACE:RegExp = /[ \t\n\r]+/g;
		/**
		 * RegExp match'ujący białe znaki na początku tekstu
		 */
		public static const STARTING_SPACES:RegExp = /^[ \t\n\r]+/;
		
		/**
		 * Zamiana elementów z namespaced xml'a na text html'owy, który można
		 * wstawić do pola tekstowego. Białe znaki są merge'owane i 
		 * obsługiwane w sposób html'owy. F-ja ta zamienia <b>tylko</b> 
		 * dzieci na tekst, nie przenosi przkazanego elementu.
		 * @param node wezeł z którego dzieci wydobyć tekst i node'y-tag'i
		 * @param wsTracker obiekt do śledzenia czy ostatni przetworzony znak
		 * był białym znakiem, jeżeli tak to białe znaki z początku danego
		 * elementu są usuwane. Zwykle to utility ustala ten obiekt
		 * @return tekst zawierający tagi'i zamienione na tekst
		 * @see #xml2html
		 */
		public static function xmlChildren2html( node:XML, wsTracker:Object=null ):String {
			if ( wsTracker==null )
				wsTracker = initWSTracker();
			
			var buf:String = "";
			for each( var child:XML in node.children() ) {
				switch ( child.nodeKind() ) {
					case "element":
						if ( child.localName()!="stylesheet" && child.localName()!="style" && child.localName()!="feedbacks" )
							buf += xml2html(child, wsTracker);
						break;
					case "text":
						buf += codeXMLChars(child.toString(), wsTracker);
						break;
				}
			}
			return buf;
		}
		
		/**
		 * Zamiana elementów z namespaced xml'a na text html'owy, który można
		 * wstawić do pola tekstowego. Białe znaki są merge'owane i 
		 * obsługiwane w sposób html'owy. F-ja ta zamienia <b>przekazany 
		 * węzeł i</b> dzieci na  tekst, nie przenosi przkazanego elementu.
		 * @param node wezeł, z którego wydobyć tekst i node'y-tag'i
		 * @param wsTracker obiekt do śledzenia czy ostatni przetworzony znak
		 * był białym znakiem, jeżeli tak to białe znaki z początku danego
		 * elementu są usuwane. Zwykle to utility ustala ten obiekt
		 * @return tekst zawierający tagi'i zamienione na tekst
		 * @see #xmlChildren2html
		 */
		public static function xml2html( node:XML, wsTracker:Object = null ):String {
			if ( wsTracker==null )
				wsTracker = initWSTracker();
			
			if ( node.localName()=="br" ) {
				wsTracker.hasSpace = true;
				return "<br/>";
			}
			// dla bloków zawsze trzeba usunąć rozpoczynające spacje
			if ( node.localName()=="p" || node.localName()=="li" )
				wsTracker.hasSpace = true;
			var buf:String = "<"+node.localName();
				
			for each( var attr:XML in node.@* ) {
				buf += " " + attr.localName()+"=\""+encodeXMLChars(attr.toString())+"\"";
			}
			buf += ">";
			
			buf += xmlChildren2html( node, wsTracker );
			
			buf += "</"+node.localName()+">";
			return buf;
		}
		
		/**
		 * F-ją usuwa powtarzające się białe znaki oraz zamienia specjalne
		 * znaki xml'owe na odpowiednie entity references.
		 * @param str tekst z xml'a z zachowanymi białymi znakami
		 * @return tekst z połączonymi białymi znakami oraz zamienionymi
		 * znakami specjalnymi.
		 */
		public static function codeXMLChars( str:String, wsTracker:Object = null ):String {
			if ( wsTracker!=null && wsTracker.hasSpace ) {
				// inicjalne białe znaki są usuwane w pień
				str = str.replace( STARTING_SPACES, "" );
			}
			// pozostałe białe znaki są łączone w jedną spację
			str = str.replace( WHITESPACE, " " );
			
			str = encodeXMLChars( str );
			
			if ( wsTracker!=null && str.length>0 )
				wsTracker.hasSpace = StringUtil.isWhitespace( str.charAt(str.length-1) );
			
			return str;
		}
		
		/**
		 * Inicjalizuje whitespace tracker'a dla f-ji codeXMLChars,
		 * xml2html i xmlChildren2html - tylko dla zaawansowanych przypadków
		 * 
		 */
		public static function initWSTracker( hasWhite:Boolean=true ):Object {
			return { hasSpace: hasWhite };
		}
		
		/**
		 * Koduje specjalne znaki xml'owe w entity references
		 */
		public static function encodeXMLChars( str:String ):String {
			str = str.replace( /&/g, "&amp;" );
			str = str.replace( /"/g, "&quot;" );
			str = str.replace( /'/g, "&apos;" );
			str = str.replace( /</g, "&lt;" );
			str = str.replace( />/g, "&gt;" );
			str = str.replace( /\uffa0/g, "\u00a0" );
			
			return str;
		}
		
		/**
		 * Koduje entity references w xml'owe znaki specjalne.
		 * @param str ciąg wejściowy
		 * @return zdekodowany ciąg znaków
		 * 
		 */
		public static function decodeXMLChars( str:String ):String {			
			str = str.replace( /&amp;/g, "&" );
			str = str.replace( /&quot;/g, "\"" );
			str = str.replace( /&apos;/g, "\'" );
			str = str.replace( /&lt;/g, "<" );
			str = str.replace( /&gt;/g, ">" );
			str = str.replace( /\u00a0/g, "\uffa0" );
			
			return str;
		}
		
		private static var xmlBases:Dictionary;
		
		/**
		 * Zapamiętuje ściezkę bazową dla danego drzewa XML'owego
		 */
		public static function setPathBase( xml:XML, path:com.yauthor.utils.IPathBase ):void {
			if ( xmlBases==null )
				xmlBases = new Dictionary(true);
			xmlBases[getRoot(xml)] = path;
		}
		
		/**
		 * Tworzy na podstawie przekazanaego węzła identyfikator.
		 * Identyfikator składa się z nazwy przekazanego węzła, 
		 * id najbliższego przodka mającego id oraz numeru w XMLList tego przodka 
		 * jaki zajmuje przekazany węzeł.
		 * @param xmlNode węzeł XML dla którego tworzymy id.
		 * @return identyfikator.
		 */
		public static function createNodeID(xmlNode:XML, unique:Boolean = false):String{
			var origNode:XML = xmlNode;
			var counter:uint = 0;
			var ln:String = String(xmlNode.localName());
			var descendandIndex:int = unique? getDescendantIndex(origNode): -1;
			var id:String = xmlNode.@id[0];
			var outputId:String;
			
			while(xmlNode != null && xmlNode.parent() != null){
				id = xmlNode.@id[0];
				
				if(id != null)
					break;
				
				xmlNode = XML(xmlNode.parent());
			}
				
			if(id == null)
				id = "root";
			
			for each(var child:XML in xmlNode.descendants(ln)){
				if(child === origNode)
					break;
				counter++;	
			}
			
			outputId = ln + "_" + id + "_" + String(counter);
			
			if(descendandIndex != -1)
				outputId = outputId.concat("_" + descendandIndex);
			
			return outputId;
		}
		
		/**
		 * Pobiera index przekazanego noda jako węzła potomka
		 * głównego węzła dokumentu XML
		 * @param xmlNode węzeł XML dla którego szukamy indexu.
		 * @return index.
		 */
		public static function getDescendantIndex(xmlNode:XML):int{
			var root:XML = getRoot(xmlNode);
			var descendands:XMLList = root.descendants(xmlNode.localName());
			var counter:int = -1;
			
			if(root == xmlNode)
				return counter;
			
			for each(var child:XML in descendands){
				counter++;
				if(child === xmlNode)
					break;
			}
			
			return counter;
		}
		
		/**
		 * Pobiera uprzednio zapamiętaną ścieżkę bazową, dla danego
		 * drzewa XML'owego.
		 */
		public static function getPathBase( xml:XML ):com.yauthor.utils.IPathBase {
			if(!xmlBases)
				return null;
			var root:XML = getRoot(xml);
			// głupie ale najpierw musze dokładnie ten sam element
			// znaleźć w dictionary
			for( var a:* in xmlBases ) {
				if ( a==root ) {
					root = a;
					break;
				}
			}
			var p:* = xmlBases[root];
			return xmlBases==null ? null : com.yauthor.utils.IPathBase(p);
		}
		
		/**
		 * Pobranie głównego elementu drzewa, do którego należy
		 * dane drzewo.
		 */
		public static function getRoot( xml:XML ):XML {
			while( xml!=null && xml.parent()!=null )
				xml = XML(xml.parent());
			return xml;
		}
		
		/**
		 * Dodaje klasę do wskazanego węzła
		 * @param xml węzeł xml'owy do którego dodać klasę
		 * @param nCls nazwa klasy, która powinna zostać dodana do węzła
		 */
		public static function addClass( xml:XML, nCls:String ):void {
			var cls:String = String(xml.attribute("class"));
			if ( StringUtil.trim(String(cls)).length==0 ) {
				xml.@["class"] = nCls;
			} else {
				var items:Array = cls.split(" ");
				if ( items.indexOf(nCls)==-1 )
					xml.@["class"] = cls+" "+nCls;
			}
		}
		
		/**
		 * Dołącza elementy z węzła source do węzła target, traktując 
		 * obydwa wezły jako mapę [nazwa-wezła] -> wezeł. Atrybuty węzłów
		 * o takich samych nazwach są łączone - atrybutu z source nadpisją
		 * atrybuty z target.
		 * @param target węzeł, do którego dołączane są elementy source
		 * @param source węzeł źródłowy
		 */
		public static function appendXML( target:XML, source:XML ):void {
			for each( var node:XML in source.elements() ) {
				var dnodes:XMLList = target.elements(node.name());
				if ( dnodes.length()>0 ) {
					var tnode:XML = dnodes[0];
					var atts:XMLList = node.@*;
					for each( var att:XML in atts ) 
						tnode.@[att.name()] = att.toString();
					appendXML( tnode, node );
				} else
					target.appendChild( node.copy() );
			}
		}
		
		
		/**
		 * Compares two given xmls.
		 */
		public static function equals(xml1:XML, xml2:XML):Boolean{
			return xml1.contains(xml2);
		}
	}
}
